<?php
/**
 * CrudCommand class file.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
 * @copyright 2008-2013 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

/**
 * CrudCommand generates code implementing CRUD operations.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @package system.cli.commands.shell
 * @since 1.0
 */
class CrudCommand extends CConsoleCommand
{
    /**
     * @var string the directory that contains templates for crud commands.
     * Defaults to null, meaning using 'framework/cli/views/shell/crud'.
     * If you set this path and some views are missing in the directory,
     * the default views will be used.
     */
    public $templatePath;
    /**
     * @var string the directory that contains functional test classes.
     * Defaults to null, meaning using 'protected/tests/functional'.
     * If this is false, it means functional test file should NOT be generated.
     */
    public $functionalTestPath;
    /**
     * @var array list of actions to be created. Each action must be associated with a template file with the same name.
     */
    public $actions = array('create', 'update', 'index', 'view', 'admin', '_form', '_view', '_search');

    public function getHelp()
    {
        return <<<EOD
USAGE
  crud <model-class> [controller-ID] ...

DESCRIPTION
  This command generates a controller and views that accomplish
  CRUD operations for the specified data model.

PARAMETERS
 * model-class: required, the name of the data model class. This can
   also be specified as a path alias (e.g. application.models.Post).
   If the model class belongs to a module, it should be specified
   as 'ModuleID.models.ClassName'.

 * controller-ID: optional, the controller ID (e.g. 'post').
   If this is not specified, the model class name will be used
   as the controller ID. In this case, if the model belongs to
   a module, the controller will also be created under the same
   module.

   If the controller should be located under a subdirectory,
   please specify the controller ID as 'path/to/ControllerID'
   (e.g. 'admin/user').

   If the controller belongs to a module (different from the module
   that the model belongs to), please specify the controller ID
   as 'ModuleID/ControllerID' or 'ModuleID/path/to/Controller'.

EXAMPLES
 * Generates CRUD for the Post model:
        crud Post

 * Generates CRUD for the Post model which belongs to module 'admin':
        crud admin.models.Post

 * Generates CRUD for the Post model. The generated controller should
   belong to module 'admin', but not the model class:
        crud Post admin/post

EOD;
    }

    /**
     * Execute the action.
     * @param array $args command line parameters specific for this command
     * @return integer|null non zero application exit code for help or null on success
     */
    public function run($args)
    {
        if (!isset($args[0])) {
            echo "Error: data model class is required.\n";
            echo $this->getHelp();
            return 1;
        }
        $module = Yii::app();
        $modelClass = $args[0];
        if (($pos = strpos($modelClass, '.')) === false)
            $modelClass = 'application.models.' . $modelClass;
        else {
            $id = substr($modelClass, 0, $pos);
            if (($m = Yii::app()->getModule($id)) !== null)
                $module = $m;
        }
        $modelClass = Yii::import($modelClass);

        if (isset($args[1])) {
            $controllerID = $args[1];
            if (($pos = strrpos($controllerID, '/')) === false) {
                $controllerClass = ucfirst($controllerID) . 'Controller';
                $controllerFile = $module->controllerPath . DIRECTORY_SEPARATOR . $controllerClass . '.php';
                $controllerID[0] = strtolower($controllerID[0]);
            } else {
                $last = substr($controllerID, $pos + 1);
                $last[0] = strtolower($last);
                $pos2 = strpos($controllerID, '/');
                $first = substr($controllerID, 0, $pos2);
                $middle = $pos === $pos2 ? '' : substr($controllerID, $pos2 + 1, $pos - $pos2);

                $controllerClass = ucfirst($last) . 'Controller';
                $controllerFile = ($middle === '' ? '' : $middle . '/') . $controllerClass . '.php';
                $controllerID = $middle === '' ? $last : $middle . '/' . $last;
                if (($m = Yii::app()->getModule($first)) !== null)
                    $module = $m;
                else {
                    $controllerFile = $first . '/' . $controllerFile;
                    $controllerID = $first . '/' . $controllerID;
                }

                $controllerFile = $module->controllerPath . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $controllerFile);
            }
        } else {
            $controllerID = $modelClass;
            $controllerClass = ucfirst($controllerID) . 'Controller';
            $controllerFile = $module->controllerPath . DIRECTORY_SEPARATOR . $controllerClass . '.php';
            $controllerID[0] = strtolower($controllerID[0]);
        }

        $templatePath = $this->templatePath === null ? YII_PATH . '/cli/views/shell/crud' : $this->templatePath;
        $functionalTestPath = $this->functionalTestPath === null ? Yii::getPathOfAlias('application.tests.functional') : $this->functionalTestPath;

        $viewPath = $module->viewPath . DIRECTORY_SEPARATOR . str_replace('.', DIRECTORY_SEPARATOR, $controllerID);
        $fixtureName = $this->pluralize($modelClass);
        $fixtureName[0] = strtolower($fixtureName);
        $list = array(
            basename($controllerFile) => array(
                'source' => $templatePath . '/controller.php',
                'target' => $controllerFile,
                'callback' => array($this, 'generateController'),
                'params' => array($controllerClass, $modelClass),
            ),
        );

        if ($functionalTestPath !== false) {
            $list[$modelClass . 'Test.php'] = array(
                'source' => $templatePath . '/test.php',
                'target' => $functionalTestPath . DIRECTORY_SEPARATOR . $modelClass . 'Test.php',
                'callback' => array($this, 'generateTest'),
                'params' => array($controllerID, $fixtureName, $modelClass),
            );
        }

        foreach ($this->actions as $action) {
            $list[$action . '.php'] = array(
                'source' => $templatePath . '/' . $action . '.php',
                'target' => $viewPath . '/' . $action . '.php',
                'callback' => array($this, 'generateView'),
                'params' => $modelClass,
            );
        }

        $this->copyFiles($list);

        if ($module instanceof CWebModule)
            $moduleID = $module->id . '/';
        else
            $moduleID = '';

        echo "\nCrud '{$controllerID}' has been successfully created. You may access it via:\n";
        echo "http://hostname/path/to/index.php?r={$moduleID}{$controllerID}\n";
    }

    public function generateController($source, $params)
    {
        list($controllerClass, $modelClass) = $params;
        $model = CActiveRecord::model($modelClass);
        $id = $model->tableSchema->primaryKey;
        if ($id === null)
            throw new ShellException(Yii::t('yii', 'Error: Table "{table}" does not have a primary key.', array('{table}' => $model->tableName())));
        elseif (is_array($id))
            throw new ShellException(Yii::t('yii', 'Error: Table "{table}" has a composite primary key which is not supported by crud command.', array('{table}' => $model->tableName())));

        if (!is_file($source)) // fall back to default ones
            $source = YII_PATH . '/cli/views/shell/crud/' . basename($source);

        return $this->renderFile($source, array(
            'ID' => $id,
            'controllerClass' => $controllerClass,
            'modelClass' => $modelClass,
        ), true);
    }

    public function generateView($source, $modelClass)
    {
        $model = CActiveRecord::model($modelClass);
        $table = $model->getTableSchema();
        $columns = $table->columns;
        if (!is_file($source)) // fall back to default ones
            $source = YII_PATH . '/cli/views/shell/crud/' . basename($source);
        return $this->renderFile($source, array(
            'ID' => $table->primaryKey,
            'modelClass' => $modelClass,
            'columns' => $columns), true);
    }

    public function generateTest($source, $params)
    {
        list($controllerID, $fixtureName, $modelClass) = $params;
        if (!is_file($source)) // fall back to default ones
            $source = YII_PATH . '/cli/views/shell/crud/' . basename($source);
        return $this->renderFile($source, array(
            'controllerID' => $controllerID,
            'fixtureName' => $fixtureName,
            'modelClass' => $modelClass,
        ), true);
    }

    public function generateInputLabel($modelClass, $column)
    {
        return "CHtml::activeLabelEx(\$model,'{$column->name}')";
    }

    public function generateInputField($modelClass, $column)
    {
        if ($column->type === 'boolean')
            return "CHtml::activeCheckBox(\$model,'{$column->name}')";
        elseif (stripos($column->dbType, 'text') !== false)
            return "CHtml::activeTextArea(\$model,'{$column->name}',array('rows'=>6, 'cols'=>50))";
        else {
            if (preg_match('/^(password|pass|passwd|passcode)$/i', $column->name))
                $inputField = 'activePasswordField';
            else
                $inputField = 'activeTextField';

            if ($column->type !== 'string' || $column->size === null)
                return "CHtml::{$inputField}(\$model,'{$column->name}')";
            else {
                if (($size = $maxLength = $column->size) > 60)
                    $size = 60;
                return "CHtml::{$inputField}(\$model,'{$column->name}',array('size'=>$size,'maxlength'=>$maxLength))";
            }
        }
    }

    public function generateActiveLabel($modelClass, $column)
    {
        return "\$form->labelEx(\$model,'{$column->name}')";
    }

    public function generateActiveField($modelClass, $column)
    {
        if ($column->type === 'boolean')
            return "\$form->checkBox(\$model,'{$column->name}')";
        elseif (stripos($column->dbType, 'text') !== false)
            return "\$form->textArea(\$model,'{$column->name}',array('rows'=>6, 'cols'=>50))";
        else {
            if (preg_match('/^(password|pass|passwd|passcode)$/i', $column->name))
                $inputField = 'passwordField';
            else
                $inputField = 'textField';

            if ($column->type !== 'string' || $column->size === null)
                return "\$form->{$inputField}(\$model,'{$column->name}')";
            else {
                if (($size = $maxLength = $column->size) > 60)
                    $size = 60;
                return "\$form->{$inputField}(\$model,'{$column->name}',array('size'=>$size,'maxlength'=>$maxLength))";
            }
        }
    }

    public function guessNameColumn($columns)
    {
        foreach ($columns as $column) {
            if (!strcasecmp($column->name, 'name'))
                return $column->name;
        }
        foreach ($columns as $column) {
            if (!strcasecmp($column->name, 'title'))
                return $column->name;
        }
        foreach ($columns as $column) {
            if ($column->isPrimaryKey)
                return $column->name;
        }
        return 'id';
    }

    public function class2id($className)
    {
        return trim(strtolower(str_replace('_', '-', preg_replace('/(?<![A-Z])[A-Z]/', '-\0', $className))), '-');
    }

    public function class2name($className, $pluralize = false)
    {
        if ($pluralize)
            $className = $this->pluralize($className);
        return ucwords(trim(strtolower(str_replace(array('-', '_'), ' ', preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $className)))));
    }
}
